#!/bin/env python

"""A software simulation of a theremin.

A 2-dimension area serves for control of the instrument; the user drags the mouse on this area to control the frequency and amplitude.  Several modes are provided that adds virtual "frets" to allow for the playing of the equal tempered tuning (or subsets of it).

For better musical sound run your sound card into a guitar amp or similar.

Requires Python 2.3+ and PyGTK 2.4+ (not tested on anything older).

http://ptheremin.sourceforge.net
"""

import array
import fcntl
import math
import ossaudiodev
import struct
import threading
import time
import wave
import random

import pygtk
pygtk.require('2.0')
import gtk
import pango
import bluetooth

SCALES = ("chromatic", "diatonic major", "pentatonic major", "pentatonic minor", "blues")
INIT_FREQ = 20

#NAME="PTheremin"
#VERSION="0.2.1"
NAME="Synphonecs"
VERSION="0.0.1"

# frequencias e volume para o player
PLAY_FREQ_MAX   = 3180 #2180
PLAY_FREQ_MIN   = 600
PLAY_FREQ_RANGE = PLAY_FREQ_MAX - PLAY_FREQ_MIN
PLAY_VOL_INIT   = 0.7
PLAY_VOL_MAX    = 1.0
PLAY_VOL_MIN    = 0.0
PLAY_VOL_RANGE  = PLAY_VOL_MAX - PLAY_VOL_MIN

# valores conforme os celulares/acelerometros usados
BT_FREQ_MIN   = -300 # cel Bonesso
BT_FREQ_MAX   = 300
BT_FREQ_RANGE = BT_FREQ_MAX - BT_FREQ_MIN
BT_VOL_MIN    = -100 # cel CK
BT_VOL_MAX    = 100
BT_VOL_RANGE  = BT_VOL_MAX - BT_VOL_MIN


# from "Musical Instrument Design" by Bart Hopkin
sharp = 1.05946
equal_temp_freqs = [16.352, 16.352*sharp, 18.354, 18.354*sharp, 20.602, 21.827, 21.827*sharp, 24.500, 24.500*sharp, 27.500, 27.500*sharp, 30.868]
equal_temp_labels = ['C*', 'C*#', 'D*', 'D*#', 'E*', 'F*', 'F*#', 'G*', 'G*#', 'A*', 'A*#', 'B*']

equal_temp_tuning = zip(equal_temp_labels, equal_temp_freqs)

diatonic_major_intervals = (0, 2, 4, 5, 7, 9, 11)
pentatonic_major_intervals = (0, 2, 4, 7, 9)
pentatonic_minor_intervals = (0, 3, 5, 7, 10)
blues_intervals = (0, 3, 5, 6, 7, 10)

# build up several octaves of notes
NOTES = []
for octave in range(11):
    for label,freq in equal_temp_tuning:
        NOTES.append((label.replace('*', "%d" % octave), (2**octave)*freq))

def just_freqs(notes):
    return [freq for label,freq in notes]


class PlaybackThread(threading.Thread):
    """A thread that manages audio playback."""

    def __init__(self, name, device):
        threading.Thread.__init__(self, name=name)
        self.name = name

        self.fs = 44100 # the sample frequency
        self.ft = INIT_FREQ # the base frequency of the instrument
        self.vol = 1

        if device != '/dev/null':
            self.dsp = ossaudiodev.open(device, 'w')
            self.dsp.setparameters(ossaudiodev.AFMT_S16_LE, 1, self.fs)

        self.paused = True
        self.alive = True
        self.recording = array.array('h') # *way* faster than a list for data access


    def run(self):
        def tone_gen(fs):
            """A tone sample generator."""
            x = 0
            pi = math.pi
            sin = math.sin
            ft = self.ft
            sample = 0
            prev_sample = 0
            while 1:
                prev_sample = sample
                sample = sin(2*pi*ft*x/fs)

                # The idea here is to keep the waveform continuous by only changing
                # the frequency at the end of the previous frequency's period.  And
                # it works!
                if ft != self.ft and 0.01 > sample > -0.01 and prev_sample < sample:
                    ft = self.ft
                    x = 0
                x += 1
                yield sample*self.vol*0.95 # don't max out the range otherwise we clip


        # to optimize loop performance, dereference everything ahead of time
        tone = tone_gen(self.fs)
        write_func = self.dsp.write
        free_func = self.dsp.obuffree
        pack_func = struct.pack

        record_func = self.recording.append

        while self.alive:
            wave = ""
            if not self.paused:
                while not free_func():
                    time.sleep(0.005)
                    pass

                clean = tone.next()
                val_f = clean
                val_i = int(val_f*(2**15 - 1))

                sample = pack_func("h", val_i)
                write_func(sample)
                #record_func(val_i)
            else:
                time.sleep(0.1)


    def stop(self):
        self.alive = False


    def set_new_freq(self, freq):
        """Updates the input frequency."""
        self.ft = freq

    def set_new_vol(self, vol):
        self.vol = vol

    def get_wav_data(self):
        return self.recording


    def clear_wav_data(self):
        self.recording = []




def iir_2pole(coeff1, coeff2):
    """A two-pole IIR filter generator from that one guy's filter design page that I always use."""

    xv = [0, 0, 0]
    yv = [0, 0, 0]
    
    def iir(sample):
        while 1:
            xv[0] = xv[1]
            xv[1] = xv[2]
            xv[2] = sample
            yv[0] = yv[1]
            yv[1] = yv[2]
            yv[2] = xv[0] + xv[2] - 2*xv[1] + coeff1*yv[0] + coeff2*yv[1]
            yield yv[2]


def discrete_tones(tones):
    """Makes a discrete-tone filter that latches to particular tones."""
       
    def filt(x):
        closest = tones[0]
        err = 500000
        mean = 0
        
        iir = iir_2pole(-.9979871157, 1.997850878)
        
        for i,tone in enumerate(tones):
            tone_err = abs(x - tone)
            if tone_err < err:
                closest = tone
                err = tone_err
            elif tone_err > err:
                if i > 0:
                    mean = (x - closest)/2
                    
                break
                
        return closest + mean

    return filt



""" synphonecs start """


""" Representa um dispositivo bluetooth. Cria um servidor para receber dados e repassa
    isto para o theremin.
"""
class BluetoothDevice():

    TYPE_FREQUENCY = 1
    TYPE_AMP = 2

    def __init__(self, theremin, myid, port, devtype):
        self.port = port
        self.theremin = theremin
        self.id = myid
        self.status = False
        self.data = None
        self.dev_name = None
        self.dev_addr = None
        self.server = None
        self.type = devtype
        self.actual_value = 0
        self.actual_value_vol = 0


    def server_alive(self):
        if self.server is None:
            return False
        else:
            return self.server.isAlive()

    # inicia thread do server para receber dados pelo bluetooth
    def start_server(self):
        self.server = BlueServer(self, "device", self.port) # TODO como fica se jah havia um server? como controla a memoria?
        #self.server.set_device(self.dev_name, self.dev_addr)
        self.server.start()
        self.dev_name, self.dev_addr = self.server.get_properties()

    # para a thread do servidor (espera ela acabar no join)
    def stop_server(self):
        if self.server_alive():
            self.server.stop()
            self.server.join()
        self.status = False

    def stop_server_signal(self):
        if self.server_alive():
            self.server.stop()

    def set_status_msg(self, message):
        self.theremin.set_status_msg(message)

    # faz parse dos dados recebidos e cria lista com eles
    def calc_value(self, vals):
        if self.type == BluetoothDevice.TYPE_FREQUENCY:
            if vals[1] is not "":
                return float(vals[1])
            else:
                return 0
        elif self.type == BluetoothDevice.TYPE_AMP:
            if vals[1] is not "":
                return float(vals[1])
            else:
                return 0

    def calc_value_vol(self, vals):
        if vals[0] is not "":
            return float(vals[0])
        else:
            return 0


    def received_data(self, data): 
        self.data = data
        vals = data.split("*")
        self.actual_value = self.calc_value(vals)
        self.actual_value_vol = self.calc_value_vol(vals)
        self.theremin.bt_received_data(self.id, data)

    def event_connected(self, name, addr):
        self.dev_name = name
        self.dev_addr = addr
        self.theremin.device_connected(self.id)

    def event_disconnected(self):
        self.dev_name = None
        self.dev_addr = None
        self.theremin.device_disconnected(self.id)

    def clear(self): 
        self.status = False
        self.dev_name = None
        self.dev_addr = None
        self.data = None


""" Classe para descoberta de dispositivos bluetooth
"""
class BlueDiscover(threading.Thread):
    def __init__(self, theremin):
        threading.Thread.__init__(self)
        self.theremin = theremin

    def run(self):
        self.theremin.enable_actions(False)
        self.theremin.set_status_msg("BlueDiscover: searching bluetooth devices...")
        nearby_devices = bluetooth.discover_devices(lookup_names = True)
    
        if len(nearby_devices) > 0:        
            self.theremin.set_status_msg("BlueDiscover: found %d devices" % len(nearby_devices))
            for addr, name in nearby_devices:
                print " %s - %s" % (addr, name)
        else:
            self.theremin.set_status_msg("BlueDiscover: no devices found")

        self.theremin.enable_actions(True)


""" Servidor bluetooth:
    Se conecta a um dispositivo bluetooth e recebe dados
"""
class BlueServer(threading.Thread):
    def __init__(self, btdevice, name, port):
        threading.Thread.__init__(self)
        self.name = name
        self.port = port
        self.btdevice = btdevice
        self.dev_name = None
        self.dev_addr = None
        self.alive = True
        self.client_sock = None
        self.server_sock = None

    def get_properties(self):
        return self.dev_name, self.dev_addr

    def run(self):
        self.btdevice.status = False
        self.btdevice.set_status_msg("BlueServer: waiting any device at port %d" % self.port)
        self.server_sock = bluetooth.BluetoothSocket( bluetooth.RFCOMM )
        self.server_sock.bind(("", self.port)) #bluetooth.PORT_ANY
        self.server_sock.listen(1)
        self.server_sock.settimeout(1)
        self.client_sock = None
        
        uuid = "94f39d29-7d6d-437d-973b-fba39e49d4ee"
        bluetooth.advertise_service( self.server_sock, "Synphonecs server %d" % (self.port),
                   service_id = uuid,
                   service_classes = [ uuid, bluetooth.SERIAL_PORT_CLASS ],
                   profiles = [ bluetooth.SERIAL_PORT_PROFILE ] )
        failed = True
        while failed and self.alive:
            try:
                self.client_sock, client_info = self.server_sock.accept()
                self.dev_addr = client_info[0]
                self.dev_name = bluetooth.lookup_name(self.dev_addr)
                failed = False
                self.btdevice.set_status_msg("BlueServer: device [%s, %s] connected at port %d" % (self.dev_name, self.dev_addr, self.port))
            except bluetooth.BluetoothError:
                failed = True

        if self.alive:
            self.btdevice.status = True
            self.event_connected()

            try:
                while self.alive:
                    data = self.client_sock.recv(100) # TODO ver tamanho "bom" para os dados
                    #data = "%.2f*%.2f*%.2f#%.2f*%.2f*%.2f#%.2f*%.2f*%.2f#" % (random.uniform(-10,10),random.uniform(-0.1,0.1),5*random.random(),random.uniform(-10,10),random.uniform(-0.1,0.1),5*random.random(),random.uniform(-10,10),random.uniform(-0.1,0.1),5*random.random())
                    if self.valid_data(data):
                        vals = data.rstrip("#").split("#")
                        val = vals[0]
                        #print "BlueServer: received from %s [%s]" % (self.dev_name, vals[0])
                        self.btdevice.received_data(vals[0])
                        #time.sleep(0.005)
            except IOError:
                pass

        self.btdevice.set_status_msg("BlueServer: disconnected device [%s, %s] at port %d" % (self.dev_name, self.dev_addr, self.port))
        self.stop()

    def event_connected(self):
        self.btdevice.event_connected(self.dev_name, self.dev_addr)
        
    def event_disconnected(self):
        self.btdevice.event_disconnected()

    def valid_data(self, data):
        asts = 0
        for char in data:
            if char is "*":
                asts = asts+1
            if char is "#":
                if asts is 2 and len(data) >= 15: # tamanho minimo pra ser um dado valido eh 15
                    return True
                else:
                    print ": DADOS INVALIDOS"
                    return False


    def stop(self):
        self.alive = False
        self.btdevice.status = False
        if self.client_sock is not None:
            self.client_sock.close()
        if self.server_sock is not None:
            self.server_sock.close()
        self.event_disconnected()

""" synphonecs end  """


class ThereminApp(object):
    """The GUI part of the theremin."""

    def delete_event(self, w, e, d=None): return False


    def destroy(self, w=None, d=None):
        for thread in self.threads.values():
            thread.stop()

        """ synphonecs start  """
        for device in self.bluetooth.values():
            device.stop_server()
        """ synphonecs end  """

        gtk.main_quit()

    # the next 5 functions were ripped from the scribblesimple.py example
    def configure_event(self, widget, event):
        # Create a new backing pixmap of the appropriate size
        x, y, width, height = widget.get_allocation()
        self.pixmap = gtk.gdk.Pixmap(widget.window, width, height)

        self.pixmap.draw_rectangle(widget.get_style().black_gc,
                              True, 0, 0, width, height)

        notes = [(label, int(float(x - self.freq_min)*width/(self.freq_max - self.freq_min))) for label,x in self.discrete_notes]
        root_notes = [(label, int(float(x - self.freq_min)*width/(self.freq_max - self.freq_min))) for label,x in self.root_notes]

        ygrid = height/10

        # this is the "intuitive" way to get the gc to be different colors... why isn't this in the pygtk tutorial???
        gc = widget.window.new_gc()
        gc.foreground = gtk.gdk.colormap_get_system().alloc_color(56360, 56360, 56360)

        # TODO when things are cleaner we need to color the root notes differently
        root_gc = widget.window.new_gc()
        root_gc.foreground = gtk.gdk.colormap_get_system().alloc_color(20000, 60000, 20000)
        root_gc.line_width = 3

        pc = widget.get_pango_context()
        layout = pango.Layout(pc)
        layout.set_font_description(pango.FontDescription("sans 8"))

        for label,x in notes:
            if len(label) == 3:
                l = label[0] + label[2]
            else:
                l = label[0]

            layout.set_text(l)

            if (label,x) in root_notes:
                self.pixmap.draw_line(root_gc, x, 0, x, height)
                self.pixmap.draw_layout(root_gc, x + 2, 0, layout)
            else:
                self.pixmap.draw_line(gc, x, 0, x, height)
                self.pixmap.draw_layout(gc, x + 2, 0, layout)

        for y in range(height):
            if y % ygrid == 0:
                self.pixmap.draw_line(gc, 0, y, width, y)

        return True


    def expose_event(self, widget, event):
        # Redraw the screen from the backing pixmap
        x , y, width, height = event.area
        widget.window.draw_drawable(widget.get_style().fg_gc[gtk.STATE_NORMAL],
                                    self.pixmap, x, y, x, y, width, height)

        return False


    def redraw_input(self, widget):
        # redraw the pixmap
        self.configure_event(widget, None)

        # force the drawing area to be redrawn
        alloc = widget.get_allocation()
        rect = gtk.gdk.Rectangle(alloc.x, alloc.y, alloc.width, alloc.height)
        widget.window.invalidate_rect(rect, True)


    def draw_brush(self, widget, x, y):
        # Draw a rectangle on the screen
        rect = (int(x-5), int(y-5), 10, 10)
        self.pixmap.draw_rectangle(widget.get_style().black_gc, True,
                                   rect[0], rect[1], rect[2], rect[3])
        widget.queue_draw_area(rect[0], rect[1], rect[2], rect[3])


    def button_press_event(self, widget, event):
        if event.button == 1 and self.pixmap != None:
            pass#self.draw_brush(widget, event.x, event.y)
        return True


    def motion_notify_event(self, widget, event):
        if event.is_hint:
            x, y, state = event.window.get_pointer()
        else:
            x = event.x
            y = event.y
            state = event.state
        
        if state & gtk.gdk.BUTTON1_MASK and self.pixmap != None:
            width, height = widget.window.get_size()

            freq = (x/float(width))*(self.freq_max - self.freq_min) + self.freq_min
            #if freq > self.freq_max:
                #freq = self.freq_max
            #if freq < self.freq_min:
                #freq = self.freq_min

            #print "vai setar freq 1 %d" % freq

            #print "calc vol %d, %d" % (height, y)
            vol = float(height - y)/float(height)
            #if vol > 1:
                #vol = 1
            #if vol < 0:
                #vol = 0

            #print "vai setar vol %f" % vol

            vol = 9*vol + 1 # scale to the range 1 - 10
            vol = math.log10(vol) # log scale

            self.set_freq(freq)
            #print "vai setar vol %d" % vol
            self.set_vol(vol)
      
        return True
        #return widget.emit("motion_notify_event", event)


    def make_menu(self):
        menu_def = """
        <ui>
          <menubar name="MenuBar">
            <menu action="File">
              <menuitem action="SaveAs"/>
              <separator/>
              <menuitem action="Quit"/>
            </menu>
            <menu action="Help">
              <menuitem action="About"/>
            </menu>
          </menubar>
          <toolbar name="ToolBar">
            <toolitem action="Play"/>
            <toolitem action="Stop"/>
            <separator/>
            <toolitem action="Find"/>
            <separator/>
            <toolitem action="BlueSock1"/>
            <toolitem action="BlueSock2"/>
          </toolbar>
        </ui>
        """

        def stop(w):
            self.threads['playback'].paused = True

        def play(w):
            self.threads['playback'].paused = False

        """ synphonecs start """

        def find(w):
            print "Search button click"
            #self.clear_bt()
            BlueDiscover(self).start()

        def blue_clear(w):
            print "BlueClear button click"
            #self.clear_bt()

        def start_socket1(w):
            self.set_status_msg("Stoping server 1 at port %d" % self.bluetooth['dev1'].port)
            self.bluetooth['dev1'].stop_server()
            self.set_status_msg("Starting server 1 at port %d" % self.bluetooth['dev1'].port)
            self.bluetooth['dev1'].start_server()
        
        def start_socket2(w):
            self.set_status_msg("Stoping server 2 at port %d" % self.bluetooth['dev2'].port)
            self.bluetooth['dev2'].stop_server()
            self.set_status_msg("Starting server 2 at port %d" % self.bluetooth['dev2'].port)
            self.bluetooth['dev2'].start_server()

        """ synphonecs end """

        # so this runs on older GTK versions (2.2?)
        try:
            self.about_dialog = gtk.AboutDialog()
            self.about_dialog.set_name(NAME)
            self.about_dialog.set_authors(["Synphonecs:"])
            self.about_dialog.set_authors(["lcdaronco@inf.ufrgs.br"])
            self.about_dialog.set_authors(["Synphonecs:\n  Diego Bonesso\n  Felipe Cecagno\n  Leonardo Crauss Daronco\n  Rubem Nakamura\n\nPTheremin:\n  nbm_clan@yahoo.com"])
            self.about_dialog.set_comments("Based on PTheremin: A software simulation of a Theremin (see http://en.wikipedia.org/wiki/Theremin) with a few added features.")
            self.about_dialog.set_version(VERSION)
            self.about_dialog.set_license("GPLv2")
            #self.about_dialog.set_logo(gtk.gdk.pixbuf_new_from_inline(len(self.logo), self.logo, False))
            about_icon = gtk.STOCK_ABOUT
            play_icon = gtk.STOCK_MEDIA_PLAY
            stop_icon = gtk.STOCK_MEDIA_STOP
            find_icon = gtk.STOCK_FIND
            clear_icon = gtk.STOCK_CLEAR
            disconnect_icon = gtk.STOCK_DISCONNECT
        except AttributeError, e:
            self.about_dialog = None
            about_icon = None
            play_icon = None
            stop_icon = None
            clear_icon = None
            disconnect_icon = None


        actions = [
        ('File', None, '_File'),
        ('SaveAs', gtk.STOCK_SAVE_AS, 'Save Recording _As...', None, 'Save recording', self.saveas),
        ('Quit', gtk.STOCK_QUIT, '_Quit', None, 'Quit', self.destroy),
        ('Help', None, '_Help'),
        ('About', about_icon, '_About', None, 'About', lambda w: self.about_dialog and self.about_dialog.show_all() and self.about_dialog.run()),
        
        ('Play', play_icon, 'Play', None, 'Play', play),
        ('Stop', stop_icon, 'Stop', None, 'Stop', stop),
        ('Find', find_icon, 'Search bluetooth devices', None, 'Search for bluetooth devices', find),
        ('BlueClear', clear_icon, 'Clear bluetooth devices', None, 'Clear bluetooth devices', blue_clear),
        ('BlueSock1', disconnect_icon, 'Start socket 1', None, 'Start socket 1', start_socket1),
        ('BlueSock2', disconnect_icon, 'Start socket 2', None, 'Start socket 2', start_socket2),
        ]
        """ synphonecs - actions incluidas """

        self.ag = gtk.ActionGroup('menu')
        self.ag.add_actions(actions)
        
        ui = gtk.UIManager()
        ui.insert_action_group(self.ag, 0)
        ui.add_ui_from_string(menu_def)

        return ui.get_widget('/MenuBar'), ui.get_widget('/ToolBar')


    def make_input_widget(self, lower, upper):
        input_frame = gtk.Frame("Control")
        input = gtk.DrawingArea()
        input.set_size_request(100, 100)

        input.show()

        # Signals used to handle backing pixmap
        input.connect("expose_event", self.expose_event)
        input.connect("configure_event", self.configure_event)

        # Event signals
        input.connect("button_press_event", self.button_press_event)

        input.set_events(gtk.gdk.EXPOSURE_MASK
                                | gtk.gdk.LEAVE_NOTIFY_MASK
                                | gtk.gdk.BUTTON_PRESS_MASK
                                | gtk.gdk.POINTER_MOTION_MASK)
                                #| gtk.gdk.POINTER_MOTION_HINT_MASK)


        input_table = gtk.Table(4, 3, False)
        input_table.attach(input, 2, 3, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL, 0, 0)
        
        def motion_notify(ruler, event):
            return ruler.emit("motion_notify_event", event)

        hrule = gtk.HRuler()
        hrule.set_range(lower, upper, lower, upper)

        input.connect_object("motion_notify_event", motion_notify, hrule)
        input_table.attach(hrule, 2, 3, 1, 2, gtk.EXPAND | gtk.SHRINK | gtk.FILL, gtk.FILL, 0, 0)

        vrule = gtk.VRuler()
        vrule.set_range(1, 0, 0, 1)
        input.connect_object("motion_notify_event", motion_notify, vrule)
        input_table.attach(vrule, 1, 2, 2, 3, gtk.FILL, gtk.EXPAND | gtk.SHRINK | gtk.FILL, 0, 0)
        input.connect("motion_notify_event", self.motion_notify_event)

        input_table.attach(gtk.Label("V\no\nl\nu\nm\ne"), 0, 1, 0, 3, gtk.FILL, gtk.EXPAND | gtk.SHRINK | gtk.FILL, 0, 0)
        input_table.attach(gtk.Label("Frequency (Hz)"), 1, 3, 0, 1, gtk.EXPAND | gtk.SHRINK | gtk.FILL, gtk.FILL, 0, 0)

        input_frame.add(input_table)

        return input_frame


    def init_ui(self):
        """All the gory details of the GUI."""

        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.set_size_request(600, 600)
        self.window.set_title(NAME)
        #self.window.set_icon(gtk.gdk.pixbuf_new_from_inline(len(self.logo), self.logo, False))
        
        # so the close button works
        self.window.connect("delete_event", self.delete_event)
        self.window.connect("destroy", self.destroy)

        self.root = gtk.VBox(False, 1)
        self.window.add(self.root)
        self.root.show()

        menubar, toolbar = self.make_menu()
        self.root.pack_start(menubar, False)
        self.root.pack_start(toolbar, False)
        
        """ synphonecs start """
        # Top box with options and bluetooth devices
        top_box = gtk.HBox(False, 1)
        self.root.pack_start(top_box, False, False, 2)

        # Options frame and its internal box
        opts_box = gtk.HBox(False, 1)
        opts_frame = gtk.Frame("Options")
        opts_frame.add(opts_box)
        top_box.pack_start(opts_frame, False, False, 5)

        mode_and_key = gtk.VBox(False, 1)
        
        mode_frame = gtk.Frame("Output mode")
        mode_frame.set_shadow_type(gtk.SHADOW_NONE)
        mode_ctls = gtk.VBox(False, 1)
        mode_frame.add(mode_ctls)
        mode_and_key.pack_start(mode_frame, False, False)
        opts_box.pack_start(mode_and_key, False, False)

        rb1 = gtk.RadioButton(None, 'continuous')
        rb1.connect("toggled", self.mode_changed, 'continuous')
        mode_ctls.pack_start(rb1, False, False)
        rb2 = gtk.RadioButton(rb1, 'discrete')
        rb2.connect("toggled", self.mode_changed, 'discrete')
        mode_ctls.pack_start(rb2, False, False)

        #scale_frame = gtk.Frame("Scale")
        #scale_frame.set_shadow_type(gtk.SHADOW_NONE)
        #scale_ctls = gtk.VBox(False, 1)
        #scale_frame.add(scale_ctls)
        #opts_box.pack_start(scale_frame, False, False)

        #first_rb = None
        #for scale in SCALES:
            #rb = gtk.RadioButton(first_rb, scale)
            #rb.connect("toggled", self.scale_changed, scale)
            #if first_rb == None:
                #first_rb = rb
                #rb.set_active(True)

            #scale_ctls.pack_start(rb, False, False)

        key_frame = gtk.Frame("Key")
        key_frame.set_shadow_type(gtk.SHADOW_NONE)
        key_ctl = gtk.combo_box_new_text()
        for key in ["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"]:
            key_ctl.append_text(key)
        key_ctl.set_active(3)
        key_ctl.connect("changed", self.key_changed, key_ctl)
        key_frame.add(key_ctl)
        mode_and_key.pack_start(key_frame, False, False)

        volume_frame = gtk.Frame("Volume")
        volume_frame.set_shadow_type(gtk.SHADOW_NONE)
        volume = gtk.VScale(gtk.Adjustment(value=7, lower=1, upper=10))
        volume.set_draw_value(False)
        volume_frame.add(volume)
        volume.set_inverted(True)
        opts_box.pack_start(volume_frame, False, False)

        volume.connect("value-changed", self.master_volume_changed)
        



        # Bluetooth frame and its internal box
        bluedevices_box = gtk.HBox(False, 1)
        bluedevices_frame = gtk.Frame("Bluetooth Options")
        bluedevices_frame.add(bluedevices_box)
        top_box.pack_start(bluedevices_frame, True, True, 5)

        # Box for the first bluetooth device (frequency)
        blue1_box = gtk.VBox(False, 1)

        #blue1_frame = gtk.Frame("Frequency device")
        #blue1_frame.set_shadow_type(gtk.SHADOW_NONE)
        #self.blue1_combo = gtk.combo_box_new_text()
        #for blue in ["Choose..."]:
            #self.blue1_combo.append_text(blue)
        #self.blue1_combo.set_active(0)
        #self.blue1_combo.connect("changed", self.bt_combo_change, 1)
        #blue1_frame.add(self.blue1_combo)

        blue1_title = gtk.Label("Dev1 (frequency)")

        self.blue1_name = gtk.Label("Name: <off>")
        self.blue1_name.set_alignment(xalign=0, yalign=0.5)

        self.blue1_addr = gtk.Label("Addr: <off>")
        self.blue1_addr.set_alignment(xalign=0, yalign=0.5)

        self.blue1_status = gtk.Label("Status: <off>")
        self.blue1_status.set_alignment(xalign=0, yalign=0.5)

        blue1_datalabel = gtk.Label("Data:")
        blue1_datalabel.set_alignment(xalign=0, yalign=0.5) 
        self.blue1_data = gtk.Label("")

        #blue1_box.pack_start(blue1_frame, False, False)
        blue1_box.pack_start(blue1_title, False, False, 5)
        blue1_box.pack_start(self.blue1_name, False, False)
        blue1_box.pack_start(self.blue1_addr, False, False)
        blue1_box.pack_start(self.blue1_status, False, False)
        blue1_box.pack_start(blue1_datalabel, False, False)
        blue1_box.pack_start(self.blue1_data, False, False)


        # box for the second bluetooth device
        blue2_box = gtk.VBox(False, 1)

        #blue2_frame = gtk.Frame("Amplitude device")
        #blue2_frame.set_shadow_type(gtk.SHADOW_NONE)
        #self.blue2_combo = gtk.combo_box_new_text()
        #for blue in ["Choose...", "Daileon", "ovo"]:
            #self.blue2_combo.append_text(blue)
        #self.blue2_combo.set_active(0)
        #self.blue2_combo.connect("changed", self.bt_combo_change, 2)
        #blue2_frame.add(self.blue2_combo)

        blue2_title = gtk.Label("Dev2 (volume)")

        self.blue2_name = gtk.Label("Name: <off>")
        self.blue2_name.set_alignment(xalign=0, yalign=0.5)

        self.blue2_addr = gtk.Label("Addr: <off>")
        self.blue2_addr.set_alignment(xalign=0, yalign=0.5)

        self.blue2_status = gtk.Label("Status: <off>")
        self.blue2_status.set_alignment(xalign=0, yalign=0.5)

        blue2_datalabel = gtk.Label("Data:")
        blue2_datalabel.set_alignment(xalign=0, yalign=0.5)
        self.blue2_data = gtk.Label("")

        #blue2_box.pack_start(blue2_frame, False, False)
        blue2_box.pack_start(blue2_title, False, False, 5)
        blue2_box.pack_start(self.blue2_name, False, False)
        blue2_box.pack_start(self.blue2_addr, False, False)
        blue2_box.pack_start(self.blue2_status, False, False)
        blue2_box.pack_start(blue2_datalabel, False, False)
        blue2_box.pack_start(self.blue2_data, False, False)


        # adding the bluetooth boxes in the options box
        bluedevices_box.pack_start(blue1_box, False, False, 5)
        bluedevices_box.pack_start(blue2_box, False, False, 5)


        #monitor_box2 = gtk.HBox(False, 1)
        #monitor_box = gtk.VBox(False, 1)
        #bluedevices_box.pack_start(monitor_box2, False, False, 10)
        #monitor_box2.pack_start(monitor_box, False, False, 10)

        #monitor_label = gtk.Label("Monitor")
        #self.monitor_freq = gtk.Label("Frequency: " + str(self.freq))
        #self.monitor_freq.set_alignment(xalign=0, yalign=0.5)
        #self.monitor_vol = gtk.Label("Volume: " + str(self.vol))
        #self.monitor_vol.set_alignment(xalign=0, yalign=0.5)
        #monitor_box.pack_start(monitor_label, False, False, 5)
        #monitor_box.pack_start(self.monitor_freq, False, False)
        #monitor_box.pack_start(self.monitor_vol, False, False)

        #self.bt_monitor_update_freq()
        #self.bt_monitor_update_vol()


        """ synphonecs end """

        #self.root.pack_start(gtk.HSeparator(), False, False)

        self.pixmap = None

        self.inputs = []
        self.inputs.append(self.make_input_widget(self.freq_min, self.freq_max))
        self.root.pack_start(self.inputs[0], True, True)

        self.window.show_all()

        # status
        self.status = gtk.Statusbar()
        self.status_context_id = self.status.get_context_id("lala")
        self.status.show()
        self.root.pack_end(self.status, False, False)


    def saveas(self, w):
        open_diag = gtk.FileChooserDialog(title="Save Recording", parent=self.window, action=gtk.FILE_CHOOSER_ACTION_SAVE,
                                          buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK))
        ffilt = gtk.FileFilter()
        ffilt.add_pattern("*.wav")

        open_diag.add_filter(ffilt)
        response = open_diag.run()

        if response == gtk.RESPONSE_OK:
            output = wave.open(open_diag.get_filename(), 'w')
            output.setnchannels(1)
            output.setsampwidth(2)
            output.setframerate(44100)

            pbar = gtk.ProgressBar()
            pbar.set_fraction(0)

            d = gtk.Dialog(title="Saving recording . . .", parent=self.window,
                           flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                           buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT))
            d.action_area.pack_start(pbar, True, True, 0)
            d.set_has_separator(True)
            d.show_all()

            abort = [False]

            def print_response(w, r):
                if abort[0] == False:
                    abort[0] = True

            d.connect("response", print_response)

            n = len(self.threads['playback'].get_wav_data())
            for i,sample in enumerate(self.threads['playback'].get_wav_data()):
                if i % 256 == 0:
                    pbar.set_fraction(float(i)/n)

                    # so that the progress bar dialog shows/updates
                    while gtk.events_pending():
                        gtk.mainiteration()

                    if abort[0]:
                        break

                output.writeframes(struct.pack('h', sample))

            output.close()

            d.destroy()

        open_diag.destroy()



    def new_tone_filter(self):
        self.root_notes = [n for i,n in enumerate(self.shifted_notes) if i % 12 == 0]

        if self.scale == 'chromatic':
            key_notes = NOTES

        elif self.scale == 'diatonic major':
            key_notes = [n for i,n in enumerate(self.shifted_notes) if i % 12 in diatonic_major_intervals]

        elif self.scale == 'pentatonic major':
            key_notes = [n for i,n in enumerate(self.shifted_notes) if i % 12 in pentatonic_major_intervals]

        elif self.scale == 'pentatonic minor':
            key_notes = [n for i,n in enumerate(self.shifted_notes) if i % 12 in pentatonic_minor_intervals]

        elif self.scale == 'blues':
            key_notes = [n for i,n in enumerate(self.shifted_notes) if i % 12 in blues_intervals]

        self.tone_filter = discrete_tones(just_freqs(key_notes))
        self.discrete_notes = key_notes

        for input in self.inputs:
            self.redraw_input(input)


    def scale_changed(self, button, scale_name):
        if button.get_active():
            self.scale = scale_name
            self.new_tone_filter()
    
    
    def mode_changed(self, button, mode):
        if button.get_active():
            self.mode = mode
            self.new_tone_filter()


    def key_changed(self, button, key):
        self.key = key.get_active_text()

        self.shifted_notes = list(NOTES)
        shifts = {
            'A': 9,
            'A#': 10,
            'B': 11,
            'C': 0,
            'C#': 1,
            'D': 2,
            'D#': 3,
            'E': 4,
            'F': 5,
            'F#': 6,
            'G': 7,
            'G#': 8,
        }

        for i in range(shifts[self.key]):
            self.shifted_notes.append(self.shifted_notes.pop(0))

        self.new_tone_filter()


    def master_volume_changed(self, slider):
        self.master_volume = math.log10(slider.get_value())
        self.set_vol(self.vol)
        print 'mv', self.master_volume


    def set_freq(self, freq):
        self.freq = freq

        if self.freq > self.freq_max:
            self.freq = self.freq_max
        if self.freq < self.freq_min:
            self.freq = self.freq_min

        """ synphonecs end """

        if self.mode == 'discrete':
            closest = self.tone_filter(self.freq)
        else:
            closest = self.freq

        #self.status.push(self.status.get_context_id("note"), "Output frequency:  %.2f Hz - volume %.2f%%" % (closest, self.vol))
        self.threads['playback'].set_new_freq(closest)
        #self.bt_monitor_update_freq()

    def set_vol(self, vol):
        #print "setando vol no theremin %d" % vol
        self.vol = vol

        if self.vol > PLAY_VOL_MAX:
            self.vol = PLAY_VOL_MAX
        if self.vol < PLAY_VOL_MIN:
            self.vol = PLAY_VOL_MIN

        self.status.push(self.status.get_context_id("note"), "Output frequency:  %.2f Hz - volume %.2f%%" % (self.freq, self.vol))
        self.threads['playback'].set_new_vol(self.vol*self.master_volume)
        #self.bt_monitor_update_vol()

    def pause(self, button):
        if button.get_active():
            self.threads['playback'].paused = False
        else:
            self.threads['playback'].paused = True



    """ synphonecs start """

    # habilita/desabilita as actions
    def enable_actions(self, status):
        self.ag.set_sensitive(status)

    # mensagem na barra de status
    def set_status_msg(self, message):
        print message
        self.status.pop(self.status_context_id)
        self.status_message_id = self.status.push(self.status_context_id, message)

    def device_connected(self, dev_id):
        self.update_bt_interface(dev_id)

    def device_disconnected(self, dev_id):
        self.update_bt_interface(dev_id)

    # atualiza a interface dos componentes bluetooth conforme dados guardados nos objetos BluetoothDevice
    def update_bt_interface(self, dev_id):
        target = None
        for device in self.bluetooth.values():
            if dev_id == device.id:
                target = device
            
        if dev_id == 1:
            data = self.blue1_data
            status = self.blue1_status
            name = self.blue1_name
            addr = self.blue1_addr
        elif dev_id == 2:
            data = self.blue2_data
            status = self.blue2_status
            name = self.blue2_name
            addr = self.blue2_addr
        else:
            data = None
            status = None
            name = None
            addr = None
            
        if target is not None:
            if data is not None:
                if target.data is not None:
                    data.set_text(target.data)
                else:
                    data.set_text("")
            if status is not None:
                if target.status:
                    status.set_text("Status: <on>")
                else:
                    status.set_text("Status: <off>")
            if name is not None:
                if target.dev_name is not None:
                    name.set_text("Name: " + target.dev_name)
                else:
                    name.set_text("Name: -")
            if addr is not None:
                if target.dev_addr is not None:
                    addr.set_text("Addr: " + target.dev_addr)
                else:
                    addr.set_text("Addr: -")
            if target.data is None:
                data.set_text("")

    # o mesmo da funcao acima mas para atualizar apenas o label data de um dispositivo bluetooth
    def update_bt_interface_data(self, dev_id):
        target = None
        for device in self.bluetooth.values():
            if dev_id == device.id:
                target = device
        if dev_id == 1:
            data = self.blue1_data
        elif dev_id == 2:
            data = self.blue2_data
        else:
            data = None
            
        if target is not None:
            if data is not None:
                if target.data is not None:
                    data.set_text(target.data)
                else:
                    data.set_text("")


    # funcao chamada quando algum server bluetooth recebe dados
    def bt_received_data(self, dev_id, data):
        target = None
        for device in self.bluetooth.values():
            if device.id == dev_id:
                target = device

        if target is not None:                
            if target.type == BluetoothDevice.TYPE_FREQUENCY:
                tmp = target.actual_value - BT_FREQ_MIN # deixar valores entre 0 e MAX (elimina negativos)
                step = PLAY_FREQ_RANGE / BT_FREQ_RANGE
                self.freq = (tmp * step) + PLAY_FREQ_MIN
                self.set_freq(self.freq) # seta nova frequencia/volume no theremin

                #tmp = target.actual_value_vol - BT_VOL_MIN
                #step = PLAY_VOL_RANGE / BT_VOL_RANGE
                #self.vol = (tmp * step) + PLAY_VOL_MIN
                #self.set_vol(self.vol)

            elif target.type == BluetoothDevice.TYPE_AMP:
                tmp = target.actual_value - BT_VOL_MIN # deixar valores entre 0 e MAX (elimina negativos)
                step = PLAY_VOL_RANGE / BT_VOL_RANGE
                self.vol = (tmp * step) + PLAY_VOL_MIN
                self.set_vol(self.vol) # seta nova frequencia/volume no theremin
                
            self.update_bt_interface_data(dev_id)
        
    # atualizacao do label com os valores de frequencia
    #def bt_monitor_update_freq(self):
        #self.monitor_freq.set_text("Frequency: %.2f Hz" % (self.freq))

    # atualizacao do label com os valores de volume
    #def bt_monitor_update_vol(self):
        #self.monitor_vol.set_text("Volume: %.2f" % (self.vol))

    """ synphonecs end """

    
    def __init__(self, device):

        self.threads = {}
        self.threads['playback'] = PlaybackThread("playback", device)

        """ synphonecs start """
        self.bluetooth = {}
        self.bluetooth['dev1'] = BluetoothDevice(self, 1, 10, BluetoothDevice.TYPE_FREQUENCY)
        self.bluetooth['dev2'] = BluetoothDevice(self, 2, 11, BluetoothDevice.TYPE_AMP)
        self.bt_devices = [] # lista de dispositivos encontrados
        """ synphonecs end """

        self.freq = INIT_FREQ
        #self.freq = 500
        self.freq_max = PLAY_FREQ_MAX
        #self.freq_max = 2180
        self.freq_min = PLAY_FREQ_MIN
        #self.freq_min = 20

        self.mode = 'continuous'
        self.scale = 'chromatic'
        self.key = 'C'
        self.shifted_notes = NOTES
        self.discrete_notes = NOTES
        self.root_notes = [x for i,x in enumerate(NOTES) if i % 12 == 0]
        self.master_volume = math.log10(7.2)
        self.vol = PLAY_VOL_INIT
        #self.vol = 0.7

        self.tone_filter = discrete_tones(just_freqs(NOTES))

        self.init_ui()
        gtk.gdk.threads_init()

        for thread in self.threads.values():
            thread.start()


    def main(self):
        gtk.gdk.threads_enter()
        gtk.main()       
        gtk.gdk.threads_leave()
       


     
def usage(pname):
    print """Usage:  %s [OPTIONS]

Options:

    --device=DEV    The device filename to open.  Defauts to /dev/dsp.
    --help          Display this help text and exit.
    """ % pname


def main():
    import getopt
    import sys

    opts, args = getopt.getopt(sys.argv[1:], '', ['device=', 'help'])

    dev = '/dev/dsp'
    for opt,val in opts:
        if opt == '--device':
            dev = val
        elif opt == '--help':
            usage(sys.argv[0])
            sys.exit(0)

    app = ThereminApp(device=dev)
    app.main()


if __name__ == '__main__': main()



